/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {
    Stmt,
    ArkMethod,
    ArkAssignStmt,
    ArkInstanceFieldRef,
    ClassSignature,
    ArkStaticInvokeExpr, ArkInvokeStmt, AbstractInvokeExpr, FullPosition, ArkArrayRef, Local
} from "arkanalyzer";
import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import {BaseChecker, BaseMetaData} from "../../checker/BaseChecker";
import {ClassMatcher, MatcherCallback, MatcherTypes, MethodMatcher} from "../../matcher/Matchers";
import {Defects, IssueReport} from "../../model/Defects";
import {Rule} from "../../model/Rule";
import {CheckerUtils} from "../../utils/checker/CheckerUtils";
import {RuleListUtil} from "../../utils/common/DefectsList";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoUnsafeMemberAccessCheck');
const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: "docs/no-unsafe-member-access-check.md",
    description: "Disallow member access on a value with type `any`"
};

export class NoUnsafeMemberAccessCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private clsMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS
    };
    private buildMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD,
        class: [this.clsMatcher],
    };

    public registerMatchers(): MatcherCallback[] {
        const matchBuildTs: MatcherCallback = {
            matcher: this.buildMatcher,
            callback: this.check
        }
        return [matchBuildTs];
    }

    public check = (targetMethod: ArkMethod): void => {
        const stmts = targetMethod.getBody()?.getCfg().getStmts() ?? [];
        if (!this.getFileExtension(targetMethod.getDeclaringArkClass().getDeclaringArkFile().getName(), 'ts')) {
            return;
        }
        for (const stmt of stmts) {
            if (stmt instanceof ArkAssignStmt) {
                const rightOp = stmt.getRightOp();
                if (rightOp instanceof ArkInstanceFieldRef) {
                    this.checkUnsafeMemberAccess(stmt, rightOp as ArkInstanceFieldRef, targetMethod);
                    this.checkFieldSignature(stmt, rightOp as ArkInstanceFieldRef);
                }
                if (rightOp instanceof ArkArrayRef) {
                    this.checkArrayIndex(stmt, rightOp);
                }
                if (rightOp instanceof ArkStaticInvokeExpr) {
                    this.getInvokeExprReport(stmt, targetMethod, rightOp);
                }
            }
            if (stmt instanceof ArkInvokeStmt) {
                const invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
                if (invokeExpr) {
                    this.getInvokeExprReport(stmt, targetMethod, invokeExpr);
                }
            }
        }
    }

    private checkUnsafeMemberAccess(stmt: ArkAssignStmt, rightOp: ArkInstanceFieldRef, targetMethod: ArkMethod): void {
        const base = rightOp.getBase();
        if (base && base.getType().getTypeString().includes('any')) {
            if ((stmt.getRightOp() as ArkInstanceFieldRef).getFieldSignature) {
                let fieldName = (stmt.getRightOp() as ArkInstanceFieldRef).getFieldSignature().getFieldName();
                this.addIssueReport(stmt, fieldName);
            }
        }
        const baseStmt = base?.getDeclaringStmt();
        if (baseStmt instanceof ArkAssignStmt) {
            if ((baseStmt.getRightOp() as ArkInstanceFieldRef).getFieldSignature) {
                const fieldSignature = (baseStmt.getRightOp() as ArkInstanceFieldRef)?.getFieldSignature();
                if (!fieldSignature) {
                    return;
                }
                const classSignature = fieldSignature.getDeclaringSignature();
                if (classSignature instanceof ClassSignature) {
                    this.checkClassFields(stmt, rightOp, classSignature, targetMethod);
                }
            }
        }
    }

    private checkClassFields(stmt: ArkAssignStmt, rightOp: ArkInstanceFieldRef,
                             classSignature: ClassSignature, targetMethod: ArkMethod): void {
        const invokeClass = targetMethod.getDeclaringArkClass().getDeclaringArkFile().getClass(classSignature);
        if (invokeClass) {
            invokeClass.getFields().forEach(field => {
                if (field.getSignature().getType().getTypeString().includes('any')) {
                    const fieldName = rightOp.getFieldSignature().getFieldName();
                    this.addIssueReport(stmt, fieldName);
                }
            });
        }
    }

    private checkFieldSignature(stmt: ArkAssignStmt, rightOp: ArkInstanceFieldRef): void {
        if ((rightOp as ArkInstanceFieldRef).getFieldSignature) {
            const fieldSignature = rightOp.getFieldSignature();
            if (fieldSignature && fieldSignature.getType().getTypeString().includes('any')) {
                const variableName = this.getVariableNameFromBrackets(stmt.getOriginalText() || '');
                const fieldName = fieldSignature.getFieldName();
                if (variableName === fieldName) {
                    this.addIssueReport(stmt, fieldName);
                }
            }
        }
    }

    private checkArrayIndex(stmt: ArkAssignStmt, rightOp: ArkArrayRef): void {
        const index = rightOp.getIndex();
        if (index && index.getType().getTypeString().includes('any')) {
            const variableName = this.getVariableNameFromBrackets(stmt.getOriginalText() || '');
            if (index instanceof Local) {
                const fieldName = index.getName();
                if (variableName === fieldName) {
                    this.addIssueReport(stmt, fieldName);
                }
            }
        }
    }

    private getInvokeExprReport(stmt: Stmt, targetMethod: ArkMethod, invokeExpr: AbstractInvokeExpr | ArkStaticInvokeExpr): void {
        let methodName = invokeExpr.getMethodSignature().getMethodSubSignature().getMethodName();
        if (methodName === 'constructor') {
            methodName = invokeExpr.getMethodSignature().getDeclaringClassSignature().getClassName();
        }
        const classes = targetMethod.getDeclaringArkClass().getDeclaringArkFile().getClasses();
        for (const invokeClazz of classes) {
            for (const field of invokeClazz.getFields()) {
                if (field.getName() === methodName && field.getType().getTypeString().includes('any')) {
                    this.addIssueReport(stmt);
                }
            }
        }
        const method = targetMethod.getDeclaringArkClass().getDeclaringArkFile().getClasses()[0].getMethods()[0];
        const bodyLocal = method.getBody()?.getLocals().get(methodName || '');
        if (bodyLocal && bodyLocal.getType().getTypeString().includes('any')) {
            this.addIssueReport(stmt);
        }
    }

    private getVariableNameFromBrackets(expression: string): string | null {
        const regex = /\[(.*?)\]/;
        const match = expression.match(regex);
        return match ? match[1] : null;
    }

    private getFileExtension(filePath: string, filetype: string): boolean {
        const match = filePath.match(/\.([0-9a-zA-Z]+)$/);
        if (match) {
            const extension = match[1];
            return extension === filetype;
        }
        return false;
    }

    private addIssueReport(stmt: Stmt, text: string = '') {
        const severity = this.rule.alert ?? this.metaData.severity;
        const warnInfo = this.getLineAndColumn(stmt, text);
        const defects = new Defects(warnInfo.line, warnInfo.startCol, warnInfo.endCol, this.metaData.description, severity, this.rule.ruleId,
            warnInfo.filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defects, undefined));
        RuleListUtil.push(defects);
    }

    private getLineAndColumn(stmt: Stmt, text: string) {
        let position: FullPosition | undefined = undefined;
        const originalPositions = stmt.getOperandOriginalPositions();
        if (originalPositions !== null && originalPositions !== undefined) {
            if (originalPositions.length > 3 && originalPositions[3] !== null && originalPositions[3] !== undefined) {
                position = originalPositions[3];
            } else if (originalPositions.length > 0 && originalPositions[0] !== null && originalPositions[0] !== undefined) {
                position = originalPositions[0];
            }
        }
        if (position) {
            const line = position.getFirstLine();
            const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
            if (arkFile) {
                let startCol = position.getLastCol() - text.length;
                const endCol = position.getLastCol();
                const filePath = arkFile.getFilePath();
                return {line, startCol, endCol, filePath};
            } else {
                logger.debug('originStmt or arkFile is null');
            }
        }
        return {line: -1, startCol: -1, endCol: -1, filePath: ''};
    }
}